package com.jpavlich.stemLife;

import com.jme3.app.SimpleApplication;
import com.jme3.asset.TextureKey;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.control.GhostControl;
import com.jme3.bullet.control.PhysicsControl;
import com.jme3.bullet.nodes.PhysicsNode;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.niftygui.NiftyJmeDisplay;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.scene.shape.Sphere.TextureMode;
import com.jme3.shadow.BasicShadowRenderer;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import de.lessvoid.nifty.Nifty;
import edu.wlu.cs.levy.CG.KDTree;
import edu.wlu.cs.levy.CG.KeyDuplicateException;
import edu.wlu.cs.levy.CG.KeyMissingException;
import edu.wlu.cs.levy.CG.KeySizeException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Main Application Class
 * @author Jaime Pavlich-Mariscal
 */
public class StemLife extends SimpleApplication {

    public static final double GENE_VARIABILITY = 5.0 / 100.0;
    public static final int INITIAL_NUM_CELLS = 100;
    public static final int INITIAL_NUM_FOOD = 0;
    public static final double MUTATION_PROBABILITY = 0.2;
    private static final Sphere sphere;
    private static final Box floor;
    public static final StemLife INSTANCE = new StemLife();

    static {

        sphere = new Sphere(32, 32, 0.4f, true, false);
        sphere.setTextureMode(TextureMode.Projected);


        floor = new Box(Vector3f.ZERO, 100f, 0.1f, 100f);
        floor.scaleTextureCoordinates(new Vector2f(3, 6));
    }

    public static void main(String[] args) {

        StemLife app = StemLife.INSTANCE;
//        app.setShowSettings(false);
        app.start();
    }
    private BulletAppState bulletAppState;
    private Material cellMat;
    private Material foodMat;
    private Material floor_mat;
    private GUIController guiController;
    private float prevSpeed = 0;
    private BasicShadowRenderer bsr;
    final List<Cell> cells = new ArrayList<Cell>();
    public KDTree<LivingAgent> foods = new KDTree<LivingAgent>(3);
    private ActionListener actionListener = new ActionListener() {

        @Override
        public void onAction(String name, boolean keyPressed, float tpf) {
            if (name.equals("Pause") && !keyPressed) {
                float tmp = bulletAppState.getSpeed();
                bulletAppState.setSpeed(prevSpeed);
                prevSpeed = tmp;
            }
        }
    };
    private ActionListener pickListener = new ActionListener() {

        @Override
        public void onAction(String name, boolean keyPressed, float tpf) {
            if (name.equals("pick") && !keyPressed) {
                CollisionResults results = new CollisionResults();
                Ray ray = new Ray(cam.getLocation(), cam.getDirection());
                rootNode.collideWith(ray, results);
                if (results.size() > 0) {
                    CollisionResult closest = results.getClosestCollision();
                    Geometry obj = closest.getGeometry();
                    LivingAgent a = obj.getControl(LivingAgent.class);
                    if (a != null) {
                        guiController.setPickedObject(a);
                    }
                }
            }
        }
    };
    private static final float FRICTION_CONSTANT = 0.2f;

    private StemLife() {
    }

    public Cell createCell(Vector3f pos, float mass) {
        Cell c = createAgent(Cell.class, sphere, cellMat, pos, Vector3f.ZERO, mass);
//        GhostControl detectionSphere = new GhostControl(new SphereCollisionShape(20f));
//        c.setDetectionSphere(detectionSphere);
//        bulletAppState.getPhysicsSpace().enableDebug(assetManager);
//        bulletAppState.getPhysicsSpace().add(detectionSphere);
//        detectionSphere.setEnabled(false);
        bulletAppState.getPhysicsSpace().addCollisionListener(c);
        c.setSleepingThresholds(0f, 0f);
        
        c.hungerThreshold = 1;
        
        
        cells.add(c);
        return c;
    }

    public Food createFood(Vector3f pos) {
        Food f = createAgent(Food.class, sphere, foodMat, pos, Vector3f.ZERO, 0f);
        indexFood(pos, f);
        return f;
    }

    public void indexFood(Vector3f pos, Food f) {
        try {
//            Vector3f pos = f.getRigidBodyControl().getPhysicsLocation();
            foods.insert(new double[]{pos.x, pos.y, pos.z}, f);
        } catch (KeySizeException ex) {
            Logger.getLogger(StemLife.class.getName()).log(Level.SEVERE, null, ex);
        } catch (KeyDuplicateException ex) {
            Logger.getLogger(StemLife.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Override
    public void simpleInitApp() {
        initPhysics();
        initCamera();
        initMaterials();
        initGUI();

        initFood();
        initCells();
//        initFloor();
//        initShadows();
        initInput();

    }

    private void initPhysics() {
        bulletAppState = new BulletAppState();
        stateManager.attach(bulletAppState);
        bulletAppState.getPhysicsSpace().setGravity(new Vector3f(0, 0, 0));

    }

    private void initInput() {
        inputManager.addMapping("pick", new MouseButtonTrigger(0));
        inputManager.addListener(pickListener, "pick");
        inputManager.addMapping("Pause", new KeyTrigger(KeyInput.KEY_P));
        inputManager.addListener(actionListener, new String[]{"Pause"});
    }

    private void initCamera() {
        cam.setLocation(new Vector3f(0f, 50f, 120f));
        cam.lookAt(Vector3f.ZERO, new Vector3f(0f, 50f, 0f));
        cam.setFrustumFar(1000);
        flyCam.setMoveSpeed(20f);
    }

    @Override
    public void simpleUpdate(float tpf) {
        // Update is only performed when bullet has speed > 0. Otherwise, simulation is paused
        if (bulletAppState.getSpeed() > 0) {
            for (int i = 0; i < cells.size(); i++) {
                LivingAgent agent = cells.get(i);

                // simulates water friction
                updateFriction(agent, tpf);
            }
        }
        guiController.update(tpf);
    }

    @Override
    public void simpleRender(RenderManager rm) {
        //TODO: add render code
    }

    /** Initialize the materials used in this scene. */
    public void initMaterials() {
        cellMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        TextureKey key = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
        key.setGenerateMips(true);
        Texture tex = assetManager.loadTexture(key);
        cellMat.setTexture("ColorMap", tex);

        foodMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        TextureKey key2 = new TextureKey("Textures/Terrain/BrickWall/BrickWall_normal.jpg");
        key2.setGenerateMips(true);
        Texture tex2 = assetManager.loadTexture(key2);
        foodMat.setTexture("ColorMap", tex2);

        floor_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.png");
        key3.setGenerateMips(true);
        Texture tex3 = assetManager.loadTexture(key3);
        tex3.setWrap(WrapMode.Repeat);
        floor_mat.setTexture("ColorMap", tex3);
    }

    /** Activate shadow casting and light direction */
    private void initShadows() {
        bsr = new BasicShadowRenderer(assetManager, 256);
        bsr.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
        viewPort.addProcessor(bsr);
        // Default mode is Off -- Every node declares own shadow mode!
        rootNode.setShadowMode(ShadowMode.Off);
    }

    private <T extends LivingAgent> T createAgent(Class<T> agentClass, Mesh mesh, Material mat, Vector3f pos, Vector3f vel, float mass) {
        try {
            T agent = agentClass.newInstance();
            agent.setMass(mass);
            if (agent != null) {
                Geometry ballGeo = new Geometry(agentClass.getName() + "@" + agent.hashCode(), mesh);
                ballGeo.setMaterial(mat);

                rootNode.attachChild(ballGeo);
                ballGeo.setLocalTranslation(pos);
                ballGeo.setShadowMode(ShadowMode.CastAndReceive);
                ballGeo.addControl(agent);
                bulletAppState.getPhysicsSpace().add(agent);

                // Set initial velocity
                agent.setLinearVelocity(vel);

            }
            return agent;
        } catch (InstantiationException ex) {
            Logger.getLogger(StemLife.class.getName()).log(Level.SEVERE, null, ex);
            System.exit(1);
            return null;
        } catch (IllegalAccessException ex) {
            Logger.getLogger(StemLife.class.getName()).log(Level.SEVERE, null, ex);
            System.exit(1);
            return null;
        }
    }

    public void destroyAgent(LivingAgent agent) {
        if (agent.getSpatial() != null) {
            agent.getSpatial().removeFromParent();
            agent.getSpatial().removeControl(agent);
        }
        
        bulletAppState.getPhysicsSpace().remove(agent);
        if (agent instanceof Cell) {
            cells.remove(agent);
        } else if (agent instanceof Food) {
            try {
                Vector3f pos = agent.getPhysicsLocation();
                double[] key = new double[]{pos.x, pos.y, pos.z};
                foods.delete(key);


            } catch (KeySizeException ex) {
                Logger.getLogger(StemLife.class.getName()).log(Level.SEVERE, null, ex);
            } catch (KeyMissingException ex) {
                Logger.getLogger(StemLife.class.getName()).log(Level.INFO, null, "Key missing");
            }
        }
    }

    /** A plus sign used as crosshairs to help the player with aiming.*/
    protected void initGUI() {
        guiNode.detachAllChildren();
        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");

        BitmapText ch = new BitmapText(guiFont, false);
        ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
        ch.setText("+");        // fake crosshairs :)
        ch.setLocalTranslation( // center
                settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,
                settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
        guiNode.attachChild(ch);


        NiftyJmeDisplay niftyDisplay = new NiftyJmeDisplay(
                assetManager, inputManager, audioRenderer, guiViewPort);
        /** Create a new NiftyGUI object */
        Nifty nifty = niftyDisplay.getNifty();

        guiController = new GUIController(nifty);
        /** Read your XML and initialize your custom ScreenController */
        nifty.fromXml("Interface/StemLifeGUI.xml", "start", guiController);
        // attach the Nifty display to the gui view port as a processor
        guiViewPort.addProcessor(niftyDisplay);
//        // disable the fly cam
//        flyCam.setDragToRotate(true);
    }

    private void initFood() {
        for (int i = 0; i < INITIAL_NUM_FOOD; i++) {
            createFood(new Vector3f(FastMath.rand.nextFloat() * 100 - 50, FastMath.rand.nextFloat() * 50, FastMath.rand.nextFloat() * 100 - 50));
        }
    }

    private void initCells() {

        for (int i = 0; i < INITIAL_NUM_CELLS; i++) {
            Vector3f pos = new Vector3f(FastMath.rand.nextFloat() * INITIAL_NUM_CELLS - 50, FastMath.rand.nextFloat() * 50, FastMath.rand.nextFloat() * INITIAL_NUM_CELLS - 50);
            createCell(pos, .1f);
        }
    }

    private void updateFriction(LivingAgent agent, float tpf) {
        Vector3f currentSpeed = agent.getLinearVelocity();
        Vector3f friction = currentSpeed.mult(-FRICTION_CONSTANT);
        agent.applyCentralForce(friction);
    }

    Cell getRandomCell() {
        int i = (int) (Math.random() * cells.size());
        return cells.get(i);
    }
}
